package net.cassite.daf4j.resource;

import net.cassite.daf4j.*;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 * data access for resources<br>
 * at version 0.1.1 supports the following operations:<br>
 * where(eq,ne,lt,gt,le,ge,like,between) and conditions about location must be set and must be eq/like<br>
 * select(count)<br>
 * update set(location=string/concat(data,string),buffer=inputStream)
 *
 * @since 0.1.1
 */
public class ResourceDataAccess implements DataAccess {
        private final Source source;
        private Set<InputStream> inputStreamsNotClosed;

        public ResourceDataAccess(Source source) {
                this.source = source;
        }

        @SuppressWarnings("unchecked")
        @Override
        public <En> En find(Class<En> entityClass, Object pkValue) {
                if (!entityClass.equals(Resource.class)) throw new UnsupportedOperationException();
                String location = (String) pkValue;
                try {
                        if (source.exists(location)) {
                                Resource resource = new Resource();
                                resource.setBuffer(new Buffer(source.provideStream(location)));
                                resource.setCreatingTime(new Date(source.creatingTime(location)));
                                resource.setLastModifiedTime(new Date(source.lastModifiedTime(location)));
                                resource.setLength(source.length(location));
                                resource.setLocation(location);
                                if (source.haveSubLocations(location)) {
                                        resource.setSubLocations(source.subLocations(location));
                                }
                                return (En) resource;
                        } else {
                                return null;
                        }
                } catch (Exception e) {
                        if (e instanceof RuntimeException) throw (RuntimeException) e;
                        throw new RuntimeException(e);
                }
        }

        private List<Resource> getResources(Where where) throws Exception {
                List<Resource> resources = new ArrayList<Resource>();
                for (String location : getLocations(where)) {
                        Resource resource = find(Resource.class, location);
                        if (null != resource)
                                resources.add(resource);
                }
                return resources;
        }

        private String validateLocation(String location) throws Exception {
                location = location.trim();
                if (location.endsWith(source.separator())) {
                        location = location.substring(0, location.length() - source.separator().length());
                }
                return location;
        }

        private long getLongValue(String field, String location) throws Exception {
                if (field.equals(Resource.CREATINGTIME)) {
                        return source.creatingTime(location);
                } else if (field.equals(Resource.LASTMODIFIEDTIME)) {
                        return source.lastModifiedTime(location);
                } else if (field.equals(Resource.LENGTH)) {
                        return source.length(location);
                } else {
                        throw new IllegalArgumentException();
                }
        }

        private Set<String> locationsFromCondition(Set<String> locations, Condition condition) throws Exception {
                Set<String> set = new HashSet<String>();
                String fieldName = DataUtils.findFieldNameByIData((IData<?>) condition.data);
                if (fieldName.equals(Resource.LOCATION)) return locations;
                if (fieldName.equals(Resource.BUFFER) || fieldName.equals(Resource.SUBLOCATIONS))
                        throw new IllegalArgumentException();

                long constant = (condition.args.get(0) instanceof Date) ? (((Date) condition.args.get(0)).getTime()) : ((Number) condition.args.get(0)).longValue();

                for (String location : locations) {
                        long dataValue = getLongValue(fieldName, location);
                        if (condition.type.equals(ConditionTypes.eq)) {
                                if (dataValue == constant) set.add(location);
                        } else if (condition.type.equals(ConditionTypes.ne)) {
                                if (dataValue != constant) set.add(location);
                        } else if (condition.type.equals(ConditionTypes.gt)) {
                                if (dataValue > constant) set.add(location);
                        } else if (condition.type.equals(ConditionTypes.lt)) {
                                if (dataValue < constant) set.add(location);
                        } else if (condition.type.equals(ConditionTypes.ge)) {
                                if (dataValue >= constant) set.add(location);
                        } else if (condition.type.equals(ConditionTypes.le)) {
                                if (dataValue <= constant) set.add(location);
                        } else if (condition.type.equals(ConditionTypes.between)) {
                                long constant2 = (condition.args.get(1) instanceof Date) ? (((Date) condition.args.get(1)).getTime()) : ((Number) condition.args.get(1)).longValue();
                                if (dataValue >= constant && dataValue <= constant2) set.add(location);
                        } else {
                                throw new UnsupportedOperationException();
                        }
                }
                return set;
        }

        private Set<String> locationFromAnd(Set<String> locations, And and) throws Exception {
                if (and.getExpBoolList().size() != 0) throw new UnsupportedOperationException();
                for (Condition condition : and.getConditionList()) {
                        locations = locationsFromCondition(locations, condition);
                }
                for (Or or : and.getOrList()) {
                        locations = locationFromOr(locations, or);
                }
                return locations;
        }

        private Set<String> locationFromOr(Set<String> locations, Or or) throws Exception {
                if (or.getExpBoolList().size() != 0) throw new UnsupportedOperationException();
                Set<String> set = new HashSet<String>();
                for (Condition condition : or.getConditionList()) {
                        set.addAll(locationsFromCondition(locations, condition));
                }
                for (And and : or.getAndList()) {
                        set.addAll(locationFromAnd(locations, and));
                }
                return set;
        }

        private Set<String> initiateLocationInfo(Condition locationCondition) throws Exception {
                Set<String> set = new HashSet<String>();
                if (locationCondition.type.equals(ConditionTypes.eq)) {
                        String location = validateLocation((String) locationCondition.args.get(0));
                        if (source.exists(location)) {
                                set.add(location);
                        }
                } else if (locationCondition.type.equals(ConditionTypes.like)) {
                        String parentLoc = validateLocation((String) ((Object[]) locationCondition.args.get(0))[0]);
                        if (source.exists(parentLoc) && source.haveSubLocations(parentLoc)) {
                                for (String sub : source.subLocations(parentLoc)) {
                                        set.add(parentLoc + source.separator() + sub);
                                }
                        }
                } else throw new IllegalArgumentException();
                return set;
        }

        private Set<String> getLocations(Where where) throws Exception {
                if (where instanceof Condition) {
                        Condition condition = (Condition) where;
                        if (!DataUtils.findFieldNameByIData((IData<?>) condition.data).equals(Resource.LOCATION)) {
                                throw new IllegalArgumentException();
                        }
                        return initiateLocationInfo(condition);
                } else if (where instanceof And) {
                        And and = (And) where;
                        Set<String> locations = null;
                        for (Condition c : and.getConditionList()) {
                                if (DataUtils.findFieldNameByIData((IData<?>) c.data).equals(Resource.LOCATION)) {
                                        locations = initiateLocationInfo(c);
                                        break;
                                }
                        }
                        if (locations == null) throw new IllegalArgumentException();
                        return locationFromAnd(locations, and);
                } else {
                        throw new IllegalArgumentException();
                }
        }

        @SuppressWarnings("unchecked")
        @Override
        public <En> List<En> list(En entity, Where whereClause, QueryParameter parameter) {
                try {
                        return (List<En>) getResources(whereClause);
                } catch (Exception e) {
                        throw new RuntimeException(e);
                }
        }

        @Override
        public List<Map<String, Object>> projection(Object entity, Where whereClause, QueryParameterWithFocus parameter) {
                try {
                        List<Resource> resources = getResources(whereClause);
                        List<Map<String, Object>> result = new ArrayList<Map<String, Object>>(1);
                        Map<String, Object> map = new LinkedHashMap<String, Object>();
                        result.add(map);
                        for (Selectable s : parameter.focusMap.keySet()) {
                                if (!(s instanceof IExpression)) throw new UnsupportedOperationException();
                                if (((IExpression) s).expType() != ExpressionTypes.count) throw new UnsupportedOperationException();
                                map.put(parameter.focusMap.get(s), resources.size());
                        }
                        return result;
                } catch (Exception e) {
                        throw new RuntimeException(e);
                }
        }

        @Override
        public void update(Object entity, Where whereClause, UpdateEntry[] entries) {
                try {
                        Set<String> locations = getLocations(whereClause);
                        for (UpdateEntry entry : entries) {
                                String fieldName = DataUtils.findFieldNameByIData(entry.data);
                                Object updateVal = entry.updateValue;
                                for (String location : locations) {
                                        if (fieldName.equals(Resource.LOCATION)) {
                                                if (updateVal instanceof IExpression) {
                                                        IExpression exp = (IExpression) updateVal;
                                                        if (exp.expType().equals(ExpressionTypes.concat)) {
                                                                if (!(exp.expArgs()[0] instanceof IData)
                                                                        || !DataUtils.findFieldNameByIData((IData<?>) exp.expArgs()[0]).equals(Resource.LOCATION)
                                                                        || !(exp.expArgs()[1] instanceof String)) throw new IllegalArgumentException();
                                                        } else {
                                                                throw new UnsupportedOperationException();
                                                        }
                                                        updateVal = location + source.separator() + exp.expArgs()[1];
                                                } else if (!(updateVal instanceof String)) throw new IllegalArgumentException();

                                                // execute
                                                source.move(location, (String) updateVal);
                                        } else if (fieldName.equals(Resource.BUFFER)) {
                                                if (!(updateVal instanceof InputStream))
                                                        throw new UnsupportedOperationException();

                                                // execute
                                                source.write(location, (InputStream) updateVal);
                                        } else throw new UnsupportedOperationException();
                                }
                        }
                } catch (Exception e) {
                        throw new RuntimeException(e);
                }
        }

        @Override
        public void remove(Object entity, Where whereClause) {
                try {
                        source.delete(getLocations(whereClause));
                } catch (Exception e) {
                        throw new RuntimeException(e);
                }
        }

        @Override
        public void save(Object[] entities) {
                for (Object e : entities) {
                        if (!(e instanceof Resource)) throw new UnsupportedOperationException();
                        InputStream stream = ((Resource) e).getBuffer().getInputStream();
                        try {
                                source.write(((Resource) e).getLocation(), stream);
                        } catch (Exception err) {
                                throw new RuntimeException(err);
                        }
                        if (inputStreamsNotClosed != null) {
                                inputStreamsNotClosed.add(stream);
                        }
                }
        }

        @Override
        public void txBegin() {
                this.inputStreamsNotClosed = new HashSet<InputStream>();
        }

        private void closeStreams() {
                for (InputStream is : inputStreamsNotClosed) {
                        try {
                                is.close();
                        } catch (IOException ignored) {
                        }
                }
                inputStreamsNotClosed = null;
        }

        @Override
        public void txCommit() {
                closeStreams();
        }

        @Override
        public void txRollback() {
                closeStreams();
        }

        @Override
        public void destroy() {
                // do nothing
        }
}
